use mlua::chunk;
use mlua::prelude::*;
use rust_embed::{EmbeddedFile, RustEmbed};
use std::str;

/// Embed everything that would otherwise be installed to datadir
#[derive(RustEmbed)]
#[folder = "."]
#[exclude = ".*"]
#[exclude = "*.a"]
#[exclude = "*.abnf"]
#[exclude = "*.ac"]
#[exclude = "*.am"]
#[exclude = "*.in"]
#[exclude = "*.json"]
#[exclude = "*.m4"]
#[exclude = "*.md"]
#[exclude = "*.nix"]
#[exclude = "*.so"]
#[exclude = "*.yml"]
#[exclude = "*~"]
#[exclude = "Makefile*"]
#[exclude = "bootstrap.sh"]
#[exclude = "config*"]
#[exclude = "libtool"]
#[exclude = "rust-toolchain"]
#[exclude = "sile*"]
#[exclude = "autom4te.cache/*"]
#[exclude = "build-aux/*"]
#[exclude = "cmake/*"]
#[exclude = "completions/*"]
#[exclude = "documentation/*"]
#[exclude = "justenough/*"]
#[exclude = "libtexpdf/*"]
#[exclude = "node_modules/*"]
#[exclude = "rusile/*"]
#[exclude = "rust-api-docs/*"]
#[exclude = "spec/*"]
#[exclude = "src/*"]
#[exclude = "target/*"]
#[exclude = "tests/*"]
// @EMBEDDED_INCLUDE_LIST@ -- this marker line gets replaced by a list of includes
pub struct SileModules;

// Link Lua loader functions from C modules that Lua would otherwise be loading externally that
// we've linked into the CLI binary. Linking happens in build-aux/build.rs.
extern "C-unwind" {
    fn luaopen_fontmetrics(lua: *mut mlua::lua_State) -> i32;
    fn luaopen_justenoughfontconfig(lua: *mut mlua::lua_State) -> i32;
    fn luaopen_justenoughharfbuzz(lua: *mut mlua::lua_State) -> i32;
    fn luaopen_justenoughicu(lua: *mut mlua::lua_State) -> i32;
    fn luaopen_justenoughlibtexpdf(lua: *mut mlua::lua_State) -> i32;
    fn luaopen_svg(lua: *mut mlua::lua_State) -> i32;
}

/// Register a Lua function in the loaders/searchers table to return C modules linked into the CLI
/// binary and another to return embedded Lua resources as Lua modules. See discussion in mlua:
/// https://github.com/khvzak/mlua/discussions/322
pub fn inject_embedded_loaders(lua: Lua) -> crate::Result<Lua> {
    let package: LuaTable = lua.globals().get("package").unwrap();
    let loaders: LuaTable = match package.get("loaders").unwrap() {
        LuaValue::Table(loaders) => loaders,
        LuaValue::Nil => package.get("searchers").unwrap(),
        _ => panic!("Unable to find appropriate interface to inject embedded loader"),
    };

    let embedded_rusile_loader = lua
        .create_function(|lua, module: String| match module.as_str() {
            "rusile" => lua
                .create_function(move |lua, _: ()| crate::get_rusile_exports(lua))
                .map(LuaValue::Function),
            _ => format!("Module '{module}' is embedded in Rust binary").into_lua(lua),
        })
        .unwrap();
    loaders.push(embedded_rusile_loader).unwrap();

    let embedded_ffi_loader = lua
        .create_function(|lua, module: String| unsafe {
            match module.as_str() {
                "fontmetrics" => lua
                    .create_c_function(luaopen_fontmetrics)
                    .map(LuaValue::Function),
                "justenoughfontconfig" => lua
                    .create_c_function(luaopen_justenoughfontconfig)
                    .map(LuaValue::Function),
                "justenoughharfbuzz" => lua
                    .create_c_function(luaopen_justenoughharfbuzz)
                    .map(LuaValue::Function),
                "justenoughicu" => lua
                    .create_c_function(luaopen_justenoughicu)
                    .map(LuaValue::Function),
                "justenoughlibtexpdf" => lua
                    .create_c_function(luaopen_justenoughlibtexpdf)
                    .map(LuaValue::Function),
                "svg" => lua.create_c_function(luaopen_svg).map(LuaValue::Function),
                _ => format!("C Module '{module}' is not linked in Rust binary").into_lua(lua),
            }
        })
        .unwrap();
    loaders.push(embedded_ffi_loader).unwrap();

    let embedded_lua_loader = lua
        .create_function(|lua, module: String| {
            let module_path = module.replace('.', "/");
            let luaversion: LuaString = lua
                .load(chunk! {
                  return _VERSION:match("%d+%.%d+")
                })
                .eval()
                .unwrap();
            let luaversion: &str = &luaversion.to_str().unwrap();
            let mut package_epath: Vec<&str> = vec!["?/init.lua", "?.lua", "lua-libraries/?.lua"];
            let path = format!("lua_modules/lib/lua/{}/?/init.lua", luaversion);
            package_epath.push(&path);
            let path = format!("lua_modules/lib/lua/{}/?.lua", luaversion);
            package_epath.push(&path);
            let path = format!("lua_modules/share/lua/{}/?/init.lua", luaversion);
            package_epath.push(&path);
            let path = format!("lua_modules/share/lua/{}/?.lua", luaversion);
            package_epath.push(&path);
            let mut resource_option: Option<EmbeddedFile> = None;
            for pattern in &package_epath {
                let path = pattern.replace('?', &module_path);
                let embedded = SileModules::get(&path);
                if embedded.is_some() {
                    resource_option = embedded;
                    break;
                }
            }
            match resource_option {
                Some(module) => lua
                    .create_function(move |lua, _: ()| {
                        let data = str::from_utf8(module.data.as_ref())
                            .expect("Embedded content is not valid UTF-8");
                        lua.load(data).call::<LuaValue>(())
                    })
                    .map(LuaValue::Function),

                None => format!("Module '{module}' is not embedded in Rust binary").into_lua(lua),
            }
        })
        .unwrap();
    loaders.push(embedded_lua_loader).unwrap();

    let embedded_ftl_loader = lua
        .create_function(|lua, module: String| {
            let module_path = module.replace('.', "/");
            let pattern = "?.ftl";
            let path = pattern.replace('?', &module_path);
            match SileModules::get(&path) {
                Some(module) => lua
                    .create_function(move |lua, _: ()| {
                        let data = str::from_utf8(module.data.as_ref())
                            .expect("Embedded content is not valid UTF-8");
                        lua.load(chunk! {
                            return assert(fluent:add_messages($data))
                        })
                        .call::<LuaValue>(())
                    })
                    .map(LuaValue::Function),
                _ => format!("FTL resource '{module_path}' is not embedded in Rust binary")
                    .into_lua(lua),
            }
        })
        .unwrap();
    loaders.push(embedded_ftl_loader).unwrap();
    Ok(lua)
}
